[TOC]
1 引言
在前面高性能代码优化规则参考一文中,我们举了一个优化的例子;但是,但是,结果优化变成劣化(恶化了),演砸了。我们继续看看到底是怎么回事?
2 优化规则评价
先复原一下场景。
光说不练假把式,每种语言运行环境都有各自的表达方式,这里用“最简单“的JavaScript做一个演示。
1  | var i,len,testData = [],testCount = 9999,repeatCount = 999;  | 
多次执行上面的代码查看效果,结果如下:
在Safari 10.1 (12603.1.30.0.34) 下的运行结果(基本稳定):
1  | origin: 163.225ms  | 
在Chrome 58.0.3029.96 下的运行结果(不太稳定):
1  | origin: 81.718017578125ms  | 
在NodeJS 7.9.0下的运行结果(不太稳定):
1  | origin: 110.480ms  | 
上面结果看来,优化循环判断条件和数据缓存都有一定的效果;但最后一种风骚的写法,看起来少了一个局部变量,但执行结果是成倍的劣化(恶化)。
这曾经也是一种代码优化的方式,作者也深中其毒,但这已经不那么重要了。现在问题来了,为什么会这么劣化,是一个值得研究的问题。
3 问题深入研究
3.1 找工具
- 搜索
v8 perf toolGoogle 第一条得到一个工具列表 - 筛选掉其中内存和CPU相关的,剩下
irhydra可以做中间代码分析的 - 我们就是想看看咱们的源码被转成什么样的代码来执行了
 
3.2 搞出来看一看
- node 本身是基于v8的,所以也不用各种下载v8源码编译什么的了,直接上
 - 使用node 生成
irhydra需要的内容 - 将上面的源代码存为
perf-opt.js - 执行下面的命令
 
1  | node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --print-opt-code perf-opt.js  | 
- 在
IRHydra2的界面导入上面生成的.asm和.cfg文件 - 可以看到我们四个函数分别对应的中间代码了
 
3.3 找文档 捋主线
- 搜索
v8 intermediate representations在得到的结果中可以得到如下的概念Crankshaft是v8的编译优化器Hydrogen是Crankshaft中的HIR(High-Level intermediate representations)中间代码,更接近于源代码,跟机器无关Lithium是Crankshaft中的LIR(Low-Level intermediate representations)中间代码,更接近于机器码,跟机器相关SSA(Static Single Assignment)是Hydrogen的描述格式,是一种组织IR(intermediate representations)的方式。
 - 搜索
Static Single Assignment可以得到一些关于SSA的参考 - 参考资料
- 《Compiled Compiler Templates for V8》
 - 《Static Single Assignment Book》
 
 - 没有找到更详尽的关于SSA的文档,比如SSA的指令及其含义说明,只有v8源码中的
src/crankshaft/hydrogen-instructions,如果你找到了,请不吝赐教(怎么找的?)。 
3.4 中间代码分析
3.4.1 工具介绍
Load Compilation Atrifacts加载.asm和.cfg文件IR标签页,打开眼睛可以看到源代码和中间代码的映射关系,深红色的竖条表示循环范围Graph标签页,打开问号可以查看控制流图的说明,点击图块可以跳转到IR标签页
3.4.2 总体结果
- 前三段代码(origin,optLen,optIdx)的控制流图完全一致,B3-B8是循环内容
 - 第三段代码(optCmp)与其他三段差别较大,
 
3.4.3 详细对比
3.4.3.1 origin 与 optLen的区别
- origin比optLen在B2块少一个
LoadNamedField t2.%length@24 Smi获取数组长度,代号i33,在循环内容之外 - origin比optLen在B3块多一个
LoadNamedField t2.%length@24 Smi获取数组长度,代号i42,每次循环都有这个操作 - origin比optLen在B5块少一个
LoadNamedField t2.%length@24 Smi获取数组长度,代号i62,每次循环都有,这个地方不科学(mark),需要用i62的地方直接使用i33就ok - 其他操作无差异
 
3.4.3.2 optLen与optIdx的区别
- optLen比optIdx在B5块多一个
var[4] = t106赋值,但这个赋值后面好像没用上。。。 
3.4.3.3 optCmp与optLen比较的异常
- 获取data[i]的时候多了
CheckMaps t2 [0x12044e7ace69](stability-check)操作 - 存储乘积结果的时候多了
CallWithDescriptor t76 t7 t16 t86 t87 s88 t74 #0 changes[*] Tagged - 异常的描述(这是代码未被编译器优化识别的警告信息)
 
1  | This instruction has side effects **unknown** to the compiler.  | 
3.5 看起来还没有找到想要的
- 上面的
SSA本身存在问题,参考意义不大 - 没有比较出前面三个代码之间的差异
 - 最后一个优化搞砸了还是没解释清楚
 
4 重头再来
4.1 使用PHP来实现(为什么?)
- 在这个例子上,PHP跟JavaScript极其相似
 - PHP是世界上最好的语言(别打脸~~)
 - PHP下用
VLD查看opcode我玩过一次,比SSA看起来更细致 
1  | <?php  | 
4.2 执行上面的PHP代码
- 将上面的内容存为
perf-opt.php - 运行
php -f perf-opt.php - 在
PHP 5.6.30上面的执行结果(稳定) 
1  | origin:4.74019000  | 
4.3 也可以使用node自带的工具来看
- 执行命令
 
1  | node --prof perf-opt.js  | 
- 转换结果
 
1  | node --prof-process isolate-xxx-v8.log > processed.log  | 
- 查看
processed.log 
5 结论
- 某些优化规则在不同的编译环境下,不一定能得到一致的效果
 - 规则是死的,但编译系统不断在改变和发展
 - 有可能我们的优化规则,跟编译器的优化冲突了,成了执行优化的障碍
 
6 参考资料
《Compiled Compiler Templates for V8》
《Static Single Assignment Book》
[WebKit] JavaScriptCore解析–高级篇(一) SSA (static single assignment)
a closer look at crankshaft, v8’s optimizing compiler
最后更新: 2022年03月02日 03:32
原始链接: http://rawbin-.github.io/performance/2017-05-07-code-optmization-ext/